// © Broadcom. All Rights Reserved.
// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
// SPDX-License-Identifier: Apache-2.0

package task

import (
	"context"

	"github.com/vmware/govmomi/property"
	"github.com/vmware/govmomi/vim25/progress"
	"github.com/vmware/govmomi/vim25/types"
)

type taskProgress struct {
	info *types.TaskInfo
}

func (t taskProgress) Percentage() float32 {
	return float32(t.info.Progress)
}

func (t taskProgress) Detail() string {
	return ""
}

func (t taskProgress) Error() error {
	if t.info.Error != nil {
		return Error{t.info.Error, t.info.Description}
	}

	return nil
}

type taskCallback struct {
	ch   chan<- progress.Report
	info *types.TaskInfo
	err  error
}

func (t *taskCallback) fn(pc []types.PropertyChange) bool {
	for _, c := range pc {
		if c.Name != "info" {
			continue
		}

		if c.Op != types.PropertyChangeOpAssign {
			continue
		}

		if c.Val == nil {
			continue
		}

		ti := c.Val.(types.TaskInfo)
		t.info = &ti
	}

	// t.info could be nil if pc can't satisfy the rules above
	if t.info == nil {
		return false
	}

	pr := taskProgress{t.info}

	// Store copy of error, so Wait() can return it as well.
	t.err = pr.Error()

	switch t.info.State {
	case types.TaskInfoStateQueued, types.TaskInfoStateRunning:
		if t.ch != nil {
			// Don't care if this is dropped
			select {
			case t.ch <- pr:
			default:
			}
		}
		return false
	case types.TaskInfoStateSuccess, types.TaskInfoStateError:
		if t.ch != nil {
			// Last one must always be delivered
			t.ch <- pr
		}
		return true
	default:
		panic("unknown state: " + t.info.State)
	}
}

// WaitEx waits for a task to finish with either success or failure. It does so
// by waiting for the "info" property of task managed object to change. The
// function returns when it finds the task in the "success" or "error" state.
// In the former case, the return value is nil. In the latter case the return
// value is an instance of this package's Error struct.
//
// Any error returned while waiting for property changes causes the function to
// return immediately and propagate the error.
//
// If the progress.Sinker argument is specified, any progress updates for the
// task are sent here. The completion percentage is passed through directly.
// The detail for the progress update is set to an empty string. If the task
// finishes in the error state, the error instance is passed through as well.
// Note that this error is the same error that is returned by this function.
func WaitEx(
	ctx context.Context,
	ref types.ManagedObjectReference,
	pc *property.Collector,
	s progress.Sinker) (*types.TaskInfo, error) {

	cb := &taskCallback{}

	// Include progress sink if specified
	if s != nil {
		cb.ch = s.Sink()
		defer close(cb.ch)
	}

	filter := &property.WaitFilter{
		WaitOptions: property.WaitOptions{
			PropagateMissing: true,
		},
	}
	filter.Add(ref, ref.Type, []string{"info"})

	if err := property.WaitForUpdatesEx(
		ctx,
		pc,
		filter,
		func(updates []types.ObjectUpdate) bool {
			for _, update := range updates {
				// Only look at updates for the expected task object.
				if update.Obj.Value == ref.Value && update.Obj.Type == ref.Type {
					if cb.fn(update.ChangeSet) {
						return true
					}
				}
			}
			return false
		}); err != nil {

		return nil, err
	}

	return cb.info, cb.err
}
